﻿using System;
using System.ComponentModel.Design;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Runtime.InteropServices;
using System.ServiceModel;
using System.ServiceModel.Channels;
using System.Threading.Tasks;
using System.Windows.Forms;
using EnvDTE;
using EnvDTE80;
using Microsoft.TeamFoundation.Client;
using Microsoft.TeamFoundation.Framework.Client;
using Microsoft.TeamFoundation.VersionControl.Client;
using Microsoft.VisualStudio;
using Microsoft.VisualStudio.CommandBars;
using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.Shell.Interop;
using Microsoft.VisualStudio.TeamFoundation;
using TfsGiggle.Core;

namespace TfsGiggle.Search
{
    /// <summary>
    /// Provides search UI for TfsGiggle searches
    /// </summary>
    [PackageRegistration(UseManagedResourcesOnly = true)]
    [InstalledProductRegistration("#110", "#112", "1.0", IconResourceID = 400)]
    [ProvideMenuResource("Menus.ctmenu", 1)]
    [ProvideAutoLoad(Microsoft.VisualStudio.Shell.Interop.UIContextGuids80.NoSolution)]
    [ProvideToolWindow(typeof(TfsGiggleSearchToolWindow))]
    [ProvideToolWindow(typeof(TfsGiggleWorkitemWindow))]
    [ProvideToolWindow(typeof(TfsGiggleChangesetWindow))]
    [Guid(GuidList.guid_TfsGiggleSearchPkgString)]
    public sealed class TfsGiggleSearchPackage : Package, IVsShellPropertyEvents
    {
        const string SearchToken = "[search]";
        internal const string TeamFoundationServerExtService = "Microsoft.VisualStudio.TeamFoundation.TeamFoundationServerExt";
        uint _EventSinkCookie;
        string currentMRUComboChoice = null;
        DTE2 dte2;
        static IVsStatusbar StatusBar;
        IAsyncSearch _search;

        /// <summary>
        /// This
        /// </summary>
        internal static TfsGiggleSearchPackage Instance;

        /// <summary>
        /// The TfsGiggle search package
        /// </summary>
        public TfsGiggleSearchPackage()
        {
            Trace.WriteLine(string.Format(CultureInfo.CurrentCulture, "Entering constructor for: {0}", this.ToString()));
            TfsGiggleSearchPackage.Instance = this;

            // http://msmvps.com/blogs/jon_skeet/archive/2011/06/22/eduasync-part-12-observing-all-exceptions.aspx
            TaskScheduler.UnobservedTaskException += (sender, e) =>
            {
                if (_search != null)
                {
                    ((IChannel)_search).Abort();
                    _search = null;
                }
                MessageBox.Show(e.Exception.Message, Resources.ToolbarName, MessageBoxButtons.OK, MessageBoxIcon.Information);
                e.SetObserved();
            };
        }

        /// <summary>
        /// Listen to something
        /// </summary>
        /// <param name="propid"></param>
        /// <param name="var"></param>
        /// <returns></returns>
        public int OnShellPropertyChange(int propid, object var)
        {
            // --- We handle the event if zombie state changes from true to false
            if ((int)__VSSPROPID.VSSPROPID_Zombie == propid)
            {
                if ((bool)var == false)
                {
                    // --- Show the commandbar
                    var cbs = ((CommandBars)dte2.CommandBars);
                    CommandBar cb = cbs["TfsGiggle Search"];
                    cb.Visible = true;

                    // --- Unsubscribe from events
                    var shellService = GetService(typeof(SVsShell)) as IVsShell;
                    if (shellService != null)
                    {
                        ErrorHandler.ThrowOnFailure(shellService.UnadviseShellPropertyChanges(_EventSinkCookie));
                    }

                    _EventSinkCookie = 0;
                }
            }

            return VSConstants.S_OK;
        }

        /// <summary>
        /// Start things up
        /// </summary>
        protected override void Initialize()
        {
            Trace.WriteLine(string.Format(CultureInfo.CurrentCulture, "Entering Initialize() of: {0}", this.ToString()));
            try
            {
                base.Initialize();

                dte2 = (DTE2)GetService(typeof(DTE));

                OleMenuCommandService mcs = GetService(typeof(IMenuCommandService)) as OleMenuCommandService;
                if (null != mcs)
                {
                    CommandID menuMyMRUComboCommandID = new CommandID(GuidList.guid_TfsGiggleSearchCmdSet, (int)PkgCmdIDList.cmdidTfsGiggleSearchCombo);
                    OleMenuCommand menuMyMRUComboCommand = new OleMenuCommand(new EventHandler(OnMenuMyMRUCombo), menuMyMRUComboCommandID);
                    menuMyMRUComboCommand.ParametersDescription = "$"; // accept any argument string
                    mcs.AddCommand(menuMyMRUComboCommand);

                    // Create the command for the tool window
                    CommandID toolwndCommandID = new CommandID(GuidList.guid_TfsGiggleSearchCmdSet, (int)PkgCmdIDList.cmdidTfsGiggleSearchTool);
                    MenuCommand menuToolWin = new MenuCommand(ShowToolWindows, toolwndCommandID);
                    mcs.AddCommand(menuToolWin);
                }

                // -- Set an event listener for shell property changes
                var shellService = GetService(typeof(SVsShell)) as IVsShell;
                if (shellService != null)
                {
                    ErrorHandler.ThrowOnFailure(shellService.AdviseShellPropertyChanges(this, out _EventSinkCookie));
                }
            }
            catch (Exception ex)
            {
                ActivityLog.LogError("Exception caught in the TfsGiggle package", ex.ToString());
                throw;
            }
        }

        internal static void SetStatusMessage(string message)
        {
            if (StatusBar == null)
            {
                StatusBar = Package.GetGlobalService(typeof(IVsStatusbar)) as IVsStatusbar;
            }
            Microsoft.VisualStudio.ErrorHandler.ThrowOnFailure(StatusBar.SetText(message));
        }

        /// <summary>
        /// Do a search
        /// </summary>
        /// <param name="userQuery"></param>
        internal void RunSearchCommand(string userQuery)
        {
            if (string.IsNullOrWhiteSpace(userQuery)) return;

            try
            {
                var tfsSearchUri = GetSearchUri();
                if (string.IsNullOrWhiteSpace(tfsSearchUri))
                {
                    return;
                }

                // Normalize the user's input
                string trimmed = userQuery.Trim();
                if (trimmed.Equals("getstats:"))
                {
                    GetStatistics(tfsSearchUri);
                    return;
                }

                var result = TfsGiggleSearch(tfsSearchUri, trimmed);
                if (result != null)
                {
                    result.ContinueWith(task => ThreadHelper.Generic.Invoke(() => ShowResults(task, userQuery)));
                }
            }
            catch (Exception ex)
            {
                ActivityLog.LogError("Exception caught in TfsGiggle", ex.ToString());
                throw;
            }
        }

        /// <summary>
        /// Get the filecontents for the document specified
        /// </summary>
        /// <param name="teamProjectCollectionId"></param>
        /// <param name="path"></param>
        /// <returns></returns>
        internal void ShowFile(string teamProjectCollectionId, string path)
        {
            var tfs = GetTfs(teamProjectCollectionId);
            if (tfs == null)
            {
                Trace.TraceInformation("ShowFile:No tfs available");
                return;
            }
            var vcs = tfs.GetService<VersionControlServer>();
            if (!vcs.ServerItemExists(path, ItemType.File))
            {
                var msg = string.Format("The file '{0}' does not exist in teamprojectcollection:{1} or you do not have access to it", path, teamProjectCollectionId);
                MessageBox.Show(msg, Resources.ToolbarName, MessageBoxButtons.OK, MessageBoxIcon.Information);
                return;
            }
            var item = vcs.GetItem(path);
            if (item == null)
            {
                var msg = string.Format("Could not get the file '{0}' in teamprojectcollection:{1} or you do not have access to it", path, teamProjectCollectionId);
                MessageBox.Show(msg, Resources.ToolbarName, MessageBoxButtons.OK, MessageBoxIcon.Information);
            }

            using (var sr = new StreamReader(item.DownloadFile()))
            {
                var tempFileName = Path.Combine(Path.GetTempPath(), Path.GetFileName(path));
                File.WriteAllText(tempFileName, sr.ReadToEnd());
                ThreadHelper.Generic.Invoke(() => OpenFile(tempFileName));
            }
        }

        /// <summary>
        /// Borrowed from Microsoft.VisualStudio.TeamFoundation.Client.dll
        /// </summary>
        /// <param name="path"></param>
        private static void OpenFile(string path)
        {
            IVsUIShellOpenDocument service = Package.GetGlobalService(typeof(SVsUIShellOpenDocument)) as IVsUIShellOpenDocument;
            Guid empty = Guid.Empty;
            Microsoft.VisualStudio.OLE.Interop.IServiceProvider provider;
            uint num;
            IVsWindowFrame frame;
            IVsUIHierarchy hierarchy;
            Microsoft.VisualStudio.ErrorHandler.ThrowOnFailure(service.OpenDocumentViaProject(path, ref empty, out provider, out hierarchy, out num, out frame));
            Microsoft.VisualStudio.ErrorHandler.ThrowOnFailure(hierarchy.SetProperty(num, -2003, path));
            if (frame != null)
            {
                Microsoft.VisualStudio.ErrorHandler.ThrowOnFailure(frame.Show());
            }
        }

        /// <summary>
        /// Do a TfsGiggle search
        /// </summary>
        /// <param name="tfsSearchUri"></param>
        /// <param name="userQuery"></param>
        /// <returns></returns>
        internal Task<QueryResultDTO> TfsGiggleSearch(string tfsSearchUri, string userQuery)
        {
            _search = new Client(tfsSearchUri).Search;
            if (_search == null)
            {
                MessageBox.Show(Resources.TfsGiggleNotAvailableFromUri, Resources.ToolbarName, MessageBoxButtons.OK, MessageBoxIcon.Information);
                Trace.TraceWarning("It would appear that TfsGiggle services are not available for {0}", tfsSearchUri);
                return null;
            }
            var result = Task<QueryResultDTO>.Factory.FromAsync(_search.BeginQuery, _search.EndQuery, userQuery, null);
            return result;
        }

        private void GetStatistics(string tfsSearchUri)
        {
            if (tfsSearchUri == null) return;
            _search = new Client(tfsSearchUri).Search;
            if (_search == null)
            {
                MessageBox.Show(Resources.TfsGiggleNotAvailableFromUri, Resources.ToolbarName, MessageBoxButtons.OK, MessageBoxIcon.Information);
                Trace.TraceWarning("It would appear that TfsGiggle services are not available for {0}", tfsSearchUri);
                return;
            }
            var statistics = Task<string>.Factory.FromAsync(_search.BeginGetStatistics, _search.EndGetStatistics, null);
            System.Threading.Tasks.Task.WaitAll(new System.Threading.Tasks.Task[] { statistics });
            DebugWrite(statistics.Result);
        }

        /// <summary>
        /// View the specified artifact
        /// </summary>
        /// <param name="teamProjectCollectionId"></param>
        /// <param name="itemId"></param>
        /// <param name="artifactType"></param>
        internal void ViewArtifact(string teamProjectCollectionId, int itemId, Type artifactType)
        {
            Trace.TraceInformation("ViewArtifact: teamProjectCollectionId:{0} itemId:{1} artifactType:{2}", teamProjectCollectionId, itemId, artifactType);
            var browserService = Package.GetGlobalService(typeof(SVsWebBrowsingService)) as IVsWebBrowsingService;
            if (browserService == null)
            {
                return;
            }

            var tfs = GetTfs(teamProjectCollectionId);
            if (tfs == null)
            {
                Trace.TraceInformation("ViewArtifact:No tfs available");
                return;
            }
            var tswa = tfs.GetService<TswaClientHyperlinkService>();
            if (tswa == null)
            {
                Trace.TraceInformation("ViewArtifact:No tswa available");
                return;
            }
            Uri uri;
            if (artifactType == typeof(TfsGiggleChangeset))
            {
                uri = tswa.GetChangesetDetailsUrl(itemId);
            }
            else
            {
                uri = tswa.GetWorkItemEditorUrl(itemId);
            }
            Trace.TraceInformation("ViewArtifact: Found a uri:{0}", uri);

            IVsWindowFrame ppFrame;

            // passing 0 to the NavigateFlags allows the browser service to reuse open instances of the internal browser.
            Microsoft.VisualStudio.ErrorHandler.ThrowOnFailure(browserService.Navigate(uri.AbsoluteUri, 0, out ppFrame));
        }

        private TfsTeamProjectCollection GetTfs(string teamProjectCollectionId)
        {
            try
            {
                var tfsUriAsString = GetSearchUri();
                if (string.IsNullOrWhiteSpace(tfsUriAsString))
                {
                    Trace.TraceInformation("GetTfs: No TfsUri available");
                    return null;
                }
                var tfsUri = new Uri(tfsUriAsString);
                var realUrl = string.Format("{0}://{1}:{2}{3}{4}", tfsUri.Scheme, tfsUri.DnsSafeHost, tfsUri.Port, tfsUri.Segments[0], tfsUri.Segments[1]);
                Trace.TraceInformation("GetTfs: realUrl:{0}", realUrl);

                var tfsConfigurationServer = TfsConfigurationServerFactory.GetConfigurationServer(new Uri(realUrl));
                if (tfsConfigurationServer == null)
                {
                    Trace.TraceInformation("GetTfs: tfsConfigurationServer is null");
                    return null;
                }
                tfsConfigurationServer.EnsureAuthenticated();
                var tpcService = tfsConfigurationServer.GetService<ITeamProjectCollectionService>();
                if (tpcService == null)
                {
                    Trace.TraceInformation("GetTfs: Could not get tpcService");
                    return null;
                }
                var collection = tpcService.GetCollection(new Guid(teamProjectCollectionId));
                var tfs = tfsConfigurationServer.GetTeamProjectCollection(collection.Id);
                return tfs;
            }
            catch (Exception exception)
            {
                MessageBox.Show("Exception caught in GetTfs:" + exception.ToString(), Resources.ToolbarName, MessageBoxButtons.OK, MessageBoxIcon.Information);
            }
            return null;
        }

        private TeamFoundationServerExt GetTFSExt()
        {
            // Get access to the TFS objects
            TeamFoundationServerExt tfsExt = this.dte2.GetObject(TeamFoundationServerExtService) as TeamFoundationServerExt;

            if (tfsExt == null)
            {
                return null;
            }

            // Make sure the user is connected to a Team Foundation Server
            if (tfsExt.ActiveProjectContext.DomainUri == null || tfsExt.ActiveProjectContext.ProjectUri == null)
            {
                MessageBox.Show(Resources.ErrorNotConnected, Resources.ToolbarName, MessageBoxButtons.OK, MessageBoxIcon.Information);
                return null;
            }
            else
            {
                return tfsExt;
            }
        }

        private void OnMenuMyMRUCombo(object sender, EventArgs e)
        {
            if (e == EventArgs.Empty)
            {
                // We should never get here; EventArgs are required.
                throw (new ArgumentException(Resources.EventArgsRequired)); // force an exception to be thrown
            }

            OleMenuCmdEventArgs eventArgs = e as OleMenuCmdEventArgs;

            if (eventArgs != null)
            {
                object input = eventArgs.InValue;
                IntPtr vOut = eventArgs.OutValue;

                if (vOut != IntPtr.Zero && input != null)
                {
                    throw (new ArgumentException(Resources.BothInOutParamsIllegal)); // force an exception to be thrown
                }
                else if (vOut != IntPtr.Zero)
                {
                    // when vOut is non-NULL, the IDE is requesting the current value for the combo
                    Marshal.GetNativeVariantForObject(this.currentMRUComboChoice, vOut);
                }
                else if (input != null)
                {
                    string newChoice = input.ToString();

                    // new value was selected or typed in
                    if (!string.IsNullOrEmpty(newChoice))
                    {
                        this.currentMRUComboChoice = newChoice;
                        RunSearchCommand(this.currentMRUComboChoice);
                    }
                    else
                    {
                        // We should never get here
                        throw (new ArgumentException(Resources.EmptyStringIllegal)); // force an exception to be thrown
                    }
                }
                else
                {
                    throw (new ArgumentException(Resources.BothInOutParamsIllegal)); // force an exception to be thrown
                }
            }
            else
            {
                // We should never get here; EventArgs are required.
                throw (new ArgumentException(Resources.EventArgsRequired)); // force an exception to be thrown
            }
        }

        private void ShowResults(Task<QueryResultDTO> task, string userQuery)
        {
            try
            {
                string statusMessage;
                var result = task.Result;
                if (_search != null)
                {
                    ((IChannel)_search).Close();
                    _search = null;
                }
                if (result.Files.Length == 0)
                {
                    Trace.TraceInformation("No results were found");
                    statusMessage = string.Format("No result(s) found for the query '{0}'. {1} documents searched in {2} ms", userQuery, result.TotalNumberOfDocumentsInTheIndex, result.GetQueryResultDTOProcessingTimeInTicks / TimeSpan.TicksPerMillisecond);
                }
                else
                {
                    Trace.TraceInformation("Results were found");
                    statusMessage = string.Format("{0} result(s) found for the query '{1}'. {2} documents searched in {3} ms", result.Files.Length, userQuery, result.TotalNumberOfDocumentsInTheIndex, result.GetQueryResultDTOProcessingTimeInTicks / TimeSpan.TicksPerMillisecond);
                    ShowToolWindows(null, null);
                    ShowWorkitems(result);
                    ShowChangesets(result);
                    ShowFilesFound(result);
                }
                SetStatusMessage(statusMessage);
            }
            catch (AggregateException exception)
            {
                exception.Handle(HandleBatchExceptions);
            }
            catch (Exception exception)
            {
                MessageBox.Show("TfsGiggle got an exception:" + exception.ToString(), Resources.ToolbarName, MessageBoxButtons.OK, MessageBoxIcon.Information);
                DebugWrite(exception.ToString());
                Trace.TraceError(exception.GetType().ToString(), exception);
            }
        }

        private void ShowWorkitems(QueryResultDTO result)
        {
            Trace.TraceInformation("ShowWorkitems");
            var window = GetToolWindow(typeof(TfsGiggleWorkitemWindow));
            if (window != null)
            {
                var workitemControl = window.Content as TfsGiggleWorkitemControl;
                if (workitemControl != null)
                {
                    workitemControl.TfsGiggleWorkitems.Clear();
                    for (int i = 0; i < result.Workitems.Length; i++)
                    {
                        workitemControl.TfsGiggleWorkitems.Add(result.Workitems[i]);
                    }
                }
            }
        }

        private void ShowChangesets(QueryResultDTO result)
        {
            Trace.TraceInformation("ShowChangsets");
            var window = GetToolWindow(typeof(TfsGiggleChangesetWindow));
            if (window != null)
            {
                var changesetControl = window.Content as TfsGiggleChangesetControl;
                if (changesetControl != null)
                {
                    changesetControl.TfsGiggleChangesets.Clear();
                    for (int i = 0; i < result.Changesets.Length; i++)
                    {
                        changesetControl.TfsGiggleChangesets.Add(result.Changesets[i]);
                    }
                }
            }
        }

        private void ShowFilesFound(QueryResultDTO result)
        {
            Trace.TraceInformation("ShowFilesFound");
            var window = GetToolWindow(typeof(TfsGiggleSearchToolWindow));
            if (window != null)
            {
                var control = window.Content as TfsGiggleSearchControl;
                if (control != null)
                {
                    control.TfsGiggleFiles.Clear();
                    for (int i = 0; i < result.Files.Length; i++)
                    {
                        control.TfsGiggleFiles.Add(result.Files[i]);
                    }
                }
            }
        }

        private bool HandleBatchExceptions(Exception exception)
        {
            Trace.TraceError("HandleBatchExceptions called");
            if (exception == null)
            {
                Trace.TraceError("exception is null");
                return true;
            }
            Trace.TraceError(exception.GetType().ToString(), exception.ToString());
            if (exception is TimeoutException)
            {
                if (_search != null)
                {
                    ((IChannel)_search).Abort();
                    _search = null;
                }
                MessageBox.Show("TfsGiggle got a timeout exception:" + exception.ToString(), Resources.ToolbarName, MessageBoxButtons.OK, MessageBoxIcon.Information);
                DebugWrite(exception.ToString());
            }
            else if (exception is CommunicationException)
            {
                if (_search != null)
                {
                    ((IChannel)_search).Abort();
                    _search = null;
                    MessageBox.Show("TfsGiggle got a communication exception:" + exception.ToString(), Resources.ToolbarName, MessageBoxButtons.OK, MessageBoxIcon.Information);
                    DebugWrite(exception.ToString());
                }
            }
            return true;
        }

        private string GetSearchUri()
        {
            TeamFoundationServerExt tfsExt = this.GetTFSExt();
            if (tfsExt == null || tfsExt.ActiveProjectContext == null)
            {
                return null;
            }
            var tfsProjectCollectionUri = tfsExt.ActiveProjectContext.DomainUri;
            Trace.TraceInformation("SearchPackage:Found tfsProjectCollectionUri:'{0}'", tfsProjectCollectionUri);
            return tfsProjectCollectionUri;
        }

        /// <summary>
        /// This function is called when the user clicks the menu item that shows the 
        /// tool window. See the Initialize method to see how the menu item is associated to 
        /// this function using the OleMenuCommandService service and the MenuCommand class.
        /// </summary>
        private void ShowToolWindows(object sender, EventArgs e)
        {
            Trace.TraceInformation("ShowToolWindows");

            var window = GetToolWindow(typeof(TfsGiggleWorkitemWindow));
            if (window != null)
            {
                Trace.TraceInformation("ShowToolWindows workitem window found");
                IVsWindowFrame windowFrame = (IVsWindowFrame)window.Frame;
                if (windowFrame != null)
                {
                    Trace.TraceInformation("ShowToolWindows workitem frame found");
                    Microsoft.VisualStudio.ErrorHandler.ThrowOnFailure(windowFrame.Show());
                }
            }

            window = GetToolWindow(typeof(TfsGiggleChangesetWindow));
            if (window != null)
            {
                Trace.TraceInformation("ShowToolWindows changeset window found");
                IVsWindowFrame windowFrame = (IVsWindowFrame)window.Frame;
                if (windowFrame != null)
                {
                    Trace.TraceInformation("ShowToolWindows workitem frame found");
                    Microsoft.VisualStudio.ErrorHandler.ThrowOnFailure(windowFrame.Show());
                }
            }

            window = GetToolWindow(typeof(TfsGiggleSearchToolWindow));
            if (window != null)
            {
                Trace.TraceInformation("ShowToolWindows window found");
                IVsWindowFrame windowFrame = (IVsWindowFrame)window.Frame;
                if (windowFrame != null)
                {
                    Trace.TraceInformation("ShowToolWindows frame found");
                    Microsoft.VisualStudio.ErrorHandler.ThrowOnFailure(windowFrame.Show());
                }
            }
        }

        private ToolWindowPane GetToolWindow(Type windowType)
        {
            // Get the instance number 0 of this tool window. This window is single instance so this instance
            // is actually the only one.
            // The last flag is set to true so that if the tool window does not exists it will be created.
            ToolWindowPane window = this.FindToolWindow(windowType, 0, true);
            if ((null == window) || (null == window.Frame))
            {
                Trace.TraceError("GetToolWindow ToolWindow not found");
                throw new NotSupportedException(Resources.CanNotCreateWindow);
            }
            return window;
        }

        private static void DebugWrite(string message)
        {
            IVsOutputWindow outWindow = Package.GetGlobalService(typeof(SVsOutputWindow)) as IVsOutputWindow;
            Guid generalPaneGuid = VSConstants.GUID_OutWindowDebugPane; // P.S. There's also the GUID_OutWindowDebugPane available. 
            IVsOutputWindowPane debugPane;
            outWindow.GetPane(ref generalPaneGuid, out debugPane);
            if (debugPane != null)
            {
                debugPane.Activate(); // Brings this pane into view 
                debugPane.OutputString(message);
            }
        }
    }
}